嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第六天!
經過前五天的學習,我們已經掌握了 Rust 的基本語法、所有權系統和參考借用。今天我們要來學習如何建立自己的資料型別—這就是結構 (Structs) 和列舉 (Enums) 的魅力所在!
如果說前面學的是 Rust 提供的「積木」,那麼今天我們就要學會如何組合這些積木,打造出符合我們需求的「城堡」。對於習慣了 C# 的 class 的我們來說,Struct 可能會讓你覺得既熟悉又陌生—熟悉的是它同樣可以組織相關的資料,陌生的是它的設計概念更偏向於「資料」而並非「行為」。
而 Enum 更是 Rust 的一大亮點!它不僅僅是一組常數的集合,而是一個功能強大的型別系統工具,能讓我們優雅地處理各種可能的情況。
讓我們從一個簡單的例子開始:
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("alice123"),
email: String::from("alice@example.com"),
age: 25,
active: true,
};
println!("使用者:{}", user1.username);
println!("信箱:{}", user1.email);
println!("年齡:{}", user1.age);
println!("狀態:{}", if user1.active { "活躍" } else { "非活躍" });
}
就像變數一樣,如果要修改結構的欄位,整個實例都必須是可變的:
fn main() {
let mut user1 = User {
username: String::from("alice123"),
email: String::from("alice@example.com"),
age: 25,
active: true,
};
user1.age = 26; // 修改年齡
user1.email = String::from("alice_new@example.com"); // 修改信箱
println!("更新後的年齡:{}", user1.age);
}
注意:Rust 不允許只將結構的某些欄位標記為可變,要嘛整個實例都可變,要嘛都不可變。
我們可以建立函式來方便地建立結構實例:
fn build_user(email: String, username: String) -> User {
User {
username: username,
email: email,
age: 18,
active: true,
}
}
// 使用欄位簡寫語法
fn build_user_short(email: String, username: String) -> User {
User {
username, // 當參數名稱與欄位名稱相同時,可以簡寫
email,
age: 18,
active: true,
}
}
fn main() {
let user2 = build_user(
String::from("bob@example.com"),
String::from("bob456")
);
println!("新用戶:{}", user2.username);
}
有時候我們想要基於既有的結構建立新的實例,只修改部分欄位:
fn main() {
let user1 = User {
username: String::from("alice123"),
email: String::from("alice@example.com"),
age: 25,
active: true,
};
// 基於 user1 建立 user2,只修改 username 和 email
let user2 = User {
username: String::from("charlie789"),
email: String::from("charlie@example.com"),
..user1 // 其餘欄位使用 user1 的值
};
println!("用戶2的年齡:{}", user2.age); // 25(從 user1 複製)
// 注意:user1 中包含 String 的欄位已經被移動,但 age 和 active 被複製
// println!("{}", user1.username); // 錯誤!username 已被移動
println!("{}", user1.age); // 沒問題,age 實現了 Copy
}
有時候我們只需要組合一些型別,但不需要為每個欄位命名:
struct Color(i32, i32, i32); // RGB 顏色
struct Point(f64, f64, f64); // 3D 座標
fn main() {
let red = Color(255, 0, 0);
let origin = Point(0.0, 0.0, 0.0);
println!("紅色的 R 值:{}", red.0);
println!("原點的 X 座標:{}", origin.0);
}
有時候我們需要一個沒有任何欄位的結構,通常用於實現 trait:
struct UnitStruct;
fn main() {
let unit = UnitStruct;
}
impl
區塊定義方法我們可以為結構定義方法和關聯函式:
#[derive(Debug)] // 讓結構可以用 {:?} 印出
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// 關聯函式(類似靜態方法)
fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
fn square(size: f64) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
// 方法(第一個參數是 &self)
fn area(&self) -> f64 {
self.width * self.height
}
fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
// 可變方法
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
// 使用關聯函式建立實例
let rect1 = Rectangle::new(30.0, 50.0);
let square = Rectangle::square(25.0);
println!("長方形:{:?}", rect1);
println!("正方形:{:?}", square);
// 使用方法
println!("長方形面積:{:.2}", rect1.area());
println!("長方形周長:{:.2}", rect1.perimeter());
println!("長方形能包含正方形嗎?{}", rect1.can_hold(&square));
// 可變方法
let mut rect2 = Rectangle::new(10.0, 20.0);
println!("縮放前:{:?}", rect2);
rect2.scale(2.0);
println!("縮放後:{:?}", rect2);
}
Rust 的列舉比許多語言的列舉都要強大得多。它不僅可以列出可能的值,每個變體還可以攜帶資料!
#[derive(Debug)]
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
println!("IPv4:{:?}", four);
println!("IPv6:{:?}", six);
}
這是 Rust 列舉的強大之處—每個變體都可以攜帶不同型別和數量的資料:
#[derive(Debug)]
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
#[derive(Debug)]
enum Message {
Quit, // 沒有資料
Move { x: i32, y: i32 }, // 命名欄位
Write(String), // 單一字串
ChangeColor(i32, i32, i32), // 三個整數
}
fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
println!("本機 IPv4:{:?}", home);
println!("環路 IPv6:{:?}", loopback);
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 0, 0);
println!("訊息:{:?}, {:?}, {:?}, {:?}", msg1, msg2, msg3, msg4);
}
列舉也可以有方法:
impl Message {
fn process(&self) {
match self {
Message::Quit => println!("收到退出訊息"),
Message::Move { x, y } => println!("移動到座標 ({}, {})", x, y),
Message::Write(text) => println!("寫入文字:{}", text),
Message::ChangeColor(r, g, b) => println!("改變顏色為 RGB({}, {}, {})", r, g, b),
}
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 20 },
Message::Write(String::from("Hello, Rust!")),
Message::ChangeColor(255, 128, 0),
];
for msg in messages {
msg.process();
}
}
Option<T>
列舉:優雅地處理空值Rust 沒有 null 值!相反,它使用 Option<T>
列舉來表示可能存在或不存在的值:
fn main() {
let some_number = Some(5);
let some_string = Some("hello");
let absent_number: Option<i32> = None;
println!("數字:{:?}", some_number);
println!("字串:{:?}", some_string);
println!("空值:{:?}", absent_number);
// 安全地處理可能為空的值
let x = Some(5);
let y = None;
println!("x + 1 = {:?}", add_one(x));
println!("y + 1 = {:?}", add_one(y));
}
fn add_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
None => None,
}
}
Option<T>
提供了許多便利方法:
fn main() {
let name = Some("Alice");
let age: Option<u32> = None;
// unwrap_or:提供預設值
println!("年齡:{}", age.unwrap_or(0));
// map:轉換 Some 中的值
let name_length = name.map(|n| n.len());
println!("名字長度:{:?}", name_length);
// is_some / is_none:檢查是否有值
if name.is_some() {
println!("有名字!");
}
if age.is_none() {
println!("沒有年齡資訊");
}
// and_then:鏈式操作
let result = name
.and_then(|n| if n.len() > 3 { Some(n.to_uppercase()) } else { None });
println!("處理結果:{:?}", result);
}
match
表達式:模式匹配的威力match
是 Rust 中處理列舉最強大的工具:
#[derive(Debug)]
enum Coin {
OneDollar, // 1元硬幣
FiveDollar, // 5元硬幣
TenDollar, // 10元硬幣
FiftyDollar(String), // 50元硬幣,攜帶發行年份或特殊版本
}
fn value_in_dollars(coin: Coin) -> u32 {
match coin {
Coin::OneDollar => {
println!("幸運硬幣!");
1
},
Coin::FiveDollar => 5,
Coin::TenDollar => 10,
Coin::FiftyDollar(version) => {
println!("來自 {} 的 50 元硬幣!", version);
50
},
}
}
fn main() {
let coins = vec![
Coin::OneDollar,
Coin::FiveDollar,
Coin::FiftyDollar(String::from("民國111年")),
Coin::TenDollar,
];
let total: u32 = coins.into_iter()
.map(|coin| value_in_dollars(coin))
.sum();
println!("總價值:{} 元", total);
}
if let
語法糖當我們只關心一個特定的模式時,可以使用 if let
:
fn main() {
let some_value = Some(3);
// 使用 match
match some_value {
Some(3) => println!("三!"),
_ => (),
}
// 使用 if let(更簡潔)
if let Some(3) = some_value {
println!("又是三!");
}
// 處理 Option
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("最大值設定為:{}", max);
}
}
讓我們用今天學到的知識建立一個簡單的部落格文章模型:這個程式展示了今天學到的所有重要概念:
1. 結構的設計:
Author
結構包含作者的基本資訊BlogPost
結構組織了文章的所有相關資料Blog
結構管理多篇文章,使用 HashMap
作為儲存2. 列舉的應用:
PostStatus
列舉表達文章的三種狀態match
和 matches!
宏來處理不同狀態3. 方法與關聯函式:
new
關聯函式作為建構函式4. Option<T>
的實際應用:
Option<&BlogPost>
if let
優雅地處理可能的空值5. 所有權與借用的實踐:
我們在例子中使用了 #[derive(Debug)]
,這是 Rust 的一個強大功能,可以自動生成常見的 trait 實現:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone(); // Clone trait
println!("{:?}", p1); // Debug trait
println!("p1 == p2: {}", p1 == p2); // PartialEq trait
}
常用的可 derive 的 trait:
Debug
:讓型別可以用 {:?}
印出Clone
:提供 clone()
方法來複製值Copy
:讓型別可以被隱式複製(只適用於簡單型別)PartialEq
:提供 ==
和 !=
運算子Eq
:表示完全相等性PartialOrd
和 Ord
:提供比較運算子今天我們學會了 Rust 中最重要的自訂型別:
結構 (Structs):
struct
組織相關資料impl
區塊為結構添加方法和關聯函式..
來基於既有實例建立新實例列舉 (Enums):
match
進行模式匹配if let
語法糖處理簡單的模式Option 型別:
實用技巧:
#[derive(...)]
自動生成常見 trait 實現明天我們將進入第一週的總結,並深入學習 Cargo 這個強大的建置工具與套件管理器。我們會學習如何管理相依性、執行測試、並為我們的專案建立良好的結構。
嘗試擴展我們的部落格模型,增加以下功能:
Comment
結構,包含作者、內容、時間戳記Category
列舉,並為文章添加分類功能Role
列舉(Admin, Editor, Writer, Reader),並為 Author
添加角色欄位提示:
Option<T>
來處理可選資料// 範例結構
struct Comment {
id: u32,
author: Author,
content: String,
// 你來完成剩下的欄位和功能!
}
enum Category {
Technology,
Lifestyle,
// 添加更多分類!
}
enum Role {
Admin,
Editor,
// 添加更多角色!
}
如果在實作過程中遇到任何問題,歡迎在留言區討論。結構和列舉是 Rust 程式設計的基礎,熟練掌握它們會讓你在後續的學習中如魚得水!
記住,好的型別設計能讓程式碼更易讀、更安全、更易維護。花時間思考如何合理地組織你的資料結構,絕對是值得的投資。
我們明天見!